/*
 * Copyright (C) 2014 Allwinner Ltd. 
 *
 * Author:
 *	Ryan Chen <ryanchen@allwinnertech.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, version 2 of the
 * License.
 *
 * File: fiv_main.c
 */
#include <linux/module.h>
#include <linux/file.h>
#include <linux/binfmts.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/err.h>
#include <linux/delay.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/crypto.h>
#include <linux/crc32.h>
#include <linux/bsearch.h>
#include <linux/spinlock.h>
#include <linux/fivm.h>

#include "fivm.h"

int fivm_debug = 0;

static int fivm_initialized;
static struct FILE_SIG *file_sig_table = NULL;
static struct FILE_SIG_HEAD *file_sig_head =  NULL ;
static const char ROOT[] = "/system";

DEFINE_MUTEX(_fid_mutex);

#ifdef FIVM_DEBUG_TIMMING
#include <linux/time.h>
static void fivm_gettime(const char *str)
{
	struct timeval	t;
	do_gettimeofday(&t);
	printk("[FIVM timer]%s: %lu.%lu\n", str,t.tv_sec, t.tv_usec);
}
#else
static void fivm_gettime(const char *str)
{
	return ;
}
#endif

static char _p_dir[1024];
static int __walk_to_root(struct file *file, char *pathname, 
			struct fivm_path *search_fp)
{
	
	struct dentry *p;
	char *start ,
		 *f_path;
	int rc = -1 ; 
	int loop, len;

	mutex_lock(&_fid_mutex);
	p = file->f_dentry; 

	start = _p_dir + sizeof(_p_dir) -1  ; /*Reserved a null for string end*/
	memset(_p_dir, 0, sizeof(_p_dir));
	for(loop = 0; loop <16 && p; loop ++, p = p->d_parent){
		if( *(p->d_name.name) == '/' )
			break;
		else{
			len = strnlen(p->d_name.name, MAX_NAME_LEN);
			start -= len ;
			memcpy( start, p->d_name.name, len );
			*(--start) ='/';
		}

	}

	dprintk("FIVM find root path : %s\n", start);

	if (!memcmp( start, ROOT, sizeof(ROOT)-1 )){ //full file path name
		len = strnlen(start, FILE_NAME_LEN );
		f_path =kmalloc(len, GFP_KERNEL);
		if(!f_path){
			derr("Out of memory\n");
			goto out ;
		}
		memcpy(f_path, start , len ) ;
		search_fp->path = f_path;
		search_fp->flag = FIVM_PART_PATH ;
		rc = 0;
	}
		

out: 
	mutex_unlock(&_fid_mutex);
	return  rc ;

}

/*
 *
 */ 
static int _find_dir( struct file *file, char *pathname, 
		struct fivm_path *search_fp)
{

	if(unlikely(!pathname)){
		printk("Null file path\n");
		return -1 ;
	}

	if( likely( *pathname == '/' ) )
		do {
			if (!memcmp( pathname, ROOT, sizeof(ROOT)-1 )) {//full file path name
				search_fp->path = pathname ;
				search_fp->flag = FIVM_FULL_PATH; 
				dprintk("Root File path: %s\n", pathname);
				return 0;
			}
		} while(0);
	else{
		return __walk_to_root(file, pathname, search_fp);
	}

	return -1 ;	

}

/*Incremental crc table*/
static int _cmp(const void * a, const void * b)
{
	const int *fa = a ;
	const struct FILE_SIG *fb = b ;

	if( *fa < fb->crc)
		return -1 ;
	if( *fa > fb->crc)
		return 1 ;
	return 0 ;
}

static struct FILE_SIG *_find_file(char *file_name)
{
	struct FILE_SIG *fs ;
	int crc , slen ; 
	size_t num, size;
	void * base;

	base = file_sig_table;
	num = file_sig_head->actual_cnt ;
	size = sizeof(struct FILE_SIG);

	slen = strnlen(file_name,FILE_NAME_LEN);
	crc = ~crc32_le( ~0, file_name, slen );
	dprintk("Find_file debug: name %s, crc 0x%x", file_name, crc);

	fs = bsearch( &crc, base, num,size,_cmp );
	if(!fs){
		derr("Find file %s crc fail\n",file_name);
		return NULL; 
	}

	if(strncmp(file_name, fs->name, slen)){
		derr("Find file %s name fail\n",file_name);
		return NULL ;
	}

	return fs ;
}

static void rel_fivm_path(struct fivm_path *fp)
{
	if(fp->flag == FIVM_PART_PATH && 
			fp->path)
		kfree(fp->path);
}

static int process_verify(struct file *file,  char *pathname)
{
	struct inode *inode = file->f_dentry->d_inode;
	struct FILE_SIG *fs ;
	int rc = 0;
	char digest[SHA_DIG_MAX];

	struct fivm_path search_fp ={ NULL, 0} ;

	fivm_gettime("Enter process verication");

	if (!fivm_initialized || !S_ISREG(inode->i_mode))
		return 0;

	rc = _find_dir(file,pathname,&search_fp);	
	if(rc != 0){
		return 0 ; /*Nothing to do, Just go on */
	}

	fivm_gettime("Find directory name ");
	fs = _find_file(search_fp.path);
	if(!fs){
		derr("Can't find file %s in sha table\n", search_fp.path);
		goto out ;
	}
	
	if(fs->flag){
		dprintk("Had verified\n");
		goto out ;
	}

	fivm_gettime("Find file name ");

	memset(digest, 0 ,SHA_DIG_MAX);
	rc = fivm_calc_hash(file,digest);
	if(rc){
		derr("fivm_calc_hash fail\n");
		goto out ;
	}

	fivm_gettime("Calc file digest ");
	if(memcmp( digest, fs->sha, SHA_DIG_MAX)){
		derr("Sha compare fail\n");
		derr("Local calculate:\n");
		print_hex_dump_bytes("", DUMP_PREFIX_NONE,
				digest, SHA_DIG_MAX)	;
		derr("Store value:\n");
		print_hex_dump_bytes("", DUMP_PREFIX_NONE,
				fs->sha,SHA_DIG_MAX )	;
		rc = -1;
		goto out ;
	}
	fs->flag =1 ;
	rc = 0;
out:
	rel_fivm_path(&search_fp);
	return rc;
}


/**
 * fivm_file_mmap 
 * @file: pointer to the file to be verify 
 * @prot: contains the protection that will be applied by the kernel.
 *
 * Return 0 on success, an error code on failure.
 */
static int _fivm_mmap_verify(struct file *file, unsigned long prot)
{
	int rc;

	if (!file )
		return 0;

	rc = process_verify(file,(char *)file->f_dentry->d_name.name) ;
	return rc;
}

/**
 * fivm_open_verify - verify file against to the hash table
 * @file: pointer to the file to be verify 
 * @mask: 
 *
 * Return 0 on success, an error code on failure.
 */
static int _fivm_open_verify(struct file *file,const char *pathname, int mask)
{
	int rc;
	if (!file || !pathname)
		return 0;

	rc = process_verify(file, (char *)pathname);
	return rc;
}

#ifdef FIVM_LKM_DEBUG 
static int _file_meta_read(
		char *filename, 
		char *buf, 
		ssize_t len,
		int offset
		)
{	
	struct file *fd;
	//ssize_t ret;
	int retLen = -1;
	mm_segment_t old_fs ;

	if(!filename || !buf){
		derr("- filename/buf NULL\n");
		return (-EINVAL);
	}

	old_fs = get_fs();
	set_fs(KERNEL_DS);

	fd = filp_open(filename, O_RDONLY, 0);

	if(IS_ERR(fd)) {
		derr(" -file open fail\n");
		return -1;
	}
	do{
		if ((fd->f_op == NULL) || (fd->f_op->read == NULL))
		{
			derr(" -file can't to open!!\n");
			break;
		} 

		if (fd->f_pos != offset) {
			if (fd->f_op->llseek) {
				if(fd->f_op->llseek(fd, offset, 0) != offset) {
					derr(" -failed to seek!!\n");
					break;
				}
			} else {
				fd->f_pos = offset;
			}
		}    		

		retLen = fd->f_op->read(fd,
				buf,
				len,
				&fd->f_pos);			

	}while(false);

	filp_close(fd, NULL);

	set_fs(old_fs);

	return retLen;
}

static const char *sig_table="/dev/block/by-name/verity_block";
static int read_file_hash_table(void)
{
	int cnt,rlen , 
		rc = -1 ,
		offset ;

	file_sig_head = kzalloc( sizeof(*file_sig_head),
			GFP_KERNEL);
	if(!file_sig_head){
		derr(" out of memory\n");
		return - ENOMEM ;
	}

	rlen = sizeof(struct FILE_SIG_HEAD); 
	offset = sizeof(struct FILE_LIST_HEAD);
	rc = _file_meta_read(
			(char *)sig_table, 
			(char *)file_sig_head, 
			rlen,
			offset);
	if( rc!= rlen  || file_sig_head->magic != FILE_SIG_MAGIC ){
		derr("Read file %s fail\n", sig_table);
		rc = - EIO;
		goto out ;
	}

    cnt = file_sig_head->actual_cnt ;
	if( cnt > DIR_MAX_FILE_NUM ){
		derr("out of files count 0x%x\n",cnt);
		rc = -1 ;
		goto out ;
	}

	rlen = sizeof(struct FILE_SIG)*cnt; 
	file_sig_table = kzalloc(rlen, GFP_KERNEL);
	if(!file_sig_table){
		derr(" out of memory\n");
		rc = - ENOMEM ;
		goto out ;
	}

	offset += sizeof(*file_sig_head);
	rc = _file_meta_read(
			(char *)sig_table,
			(char *)file_sig_table, 
			rlen, 
			offset 
			);

	if( rc!= rlen){ 
		derr("Read file %s fail\n", sig_table);
		rc =- EIO;
		goto out ;
	}

	rc = 0 ;
out :
	if(file_sig_table)
		kfree(file_sig_table);
	if(file_sig_head )
		kfree(file_sig_head);
	return rc ;

}
#endif

extern int fivm_register_func( 
		int (* mmap_verify)(struct file *, unsigned long ),
		int (* open_verify)(struct file *,const char *, int)
		);
extern int fivm_unregister_func(void) ;

raw_spinlock_t fivm_hook_lock ;
int fivm_enable(void)
{
#ifdef FIVM_LKM_DEBUG 
	int rc ;
	rc = read_file_hash_table();
	if(rc <0){
		derr("read hash table fail\n");
		return -1 ;	
	}
#endif
	raw_spin_lock(&fivm_hook_lock);
	fivm_register_func(_fivm_mmap_verify,_fivm_open_verify);
	fivm_initialized = 1;
	raw_spin_unlock(&fivm_hook_lock);

	return 0 ;
}

int  fivm_disable(void)
{
	raw_spin_lock(&fivm_hook_lock);
	fivm_unregister_func();	
	fivm_initialized = 0;
	raw_spin_unlock(&fivm_hook_lock);
	return 0 ;
}

/*Get fivm table from user*/
int fivm_set(unsigned long arg)
{
	void *param[4];
	void *sh, *st;
	size_t sh_size, st_size; 

	if( ! access_ok(VERIFY_READ,arg,4) ){
		derr("fivm param error\n");
		return -EFAULT;
	}

	memcpy(param, (void *)arg, sizeof(param));
	print_hex_dump_bytes("fivm io param buf:", DUMP_PREFIX_NONE,
			param, sizeof(param))	;

	sh= param[0];
	sh_size=(size_t)param[1];
	st=param[2];
	st_size=(size_t)param[3];

	if( sh_size != sizeof(*file_sig_head) ||
		st_size > FILE_NAME_LEN * DIR_MAX_FILE_NUM ){
		derr("fivm set buf size error %d:%d\n",
				sh_size, st_size);
		return - EFAULT;
	}

	file_sig_head = (struct FILE_SIG_HEAD *)kzalloc(sh_size,GFP_KERNEL);
	if(!file_sig_head || file_sig_table){
		derr("out of memory \n");
		return -EFAULT ;
	}

	if( copy_from_user(file_sig_head, (void __user *) sh, sh_size) ){
		derr("fivm sig head buffer from user fail\n");
		kfree(file_sig_head);
		return -EFAULT; 
	}
	
	if(st_size != file_sig_head->actual_cnt * sizeof(struct FILE_SIG)){
		derr("wrong fivm sig table count\n");
		kfree(file_sig_head);
		return -EFAULT ;
	}
	file_sig_table = kzalloc(st_size,GFP_KERNEL);
	if(!file_sig_table){
		derr("out of memory file_sig_table\n");
		kfree(file_sig_head);
		return -EFAULT ;
	}

	if( copy_from_user(file_sig_table,(void __user *) st, st_size)){
		derr("fivm sig table buffer from user fail\n");
		kfree(file_sig_head);
		kfree(file_sig_table);
		return -EFAULT; 
	}

	if(fivm_debug ==1 ){
		print_hex_dump_bytes("fivm io sig head buf:", DUMP_PREFIX_NONE,
				file_sig_head, sh_size )	;
		print_hex_dump_bytes("fivm io sig table buf:", DUMP_PREFIX_NONE,
				file_sig_table, 16 * sizeof(struct FILE_SIG) )	;
	}
	return 0 ;
}

int fivm_init(void)
{
	printk("FIVM init\n");
	raw_spin_lock_init(&fivm_hook_lock);
	return 0 ;
}

int fivm_cleanup(void)
{
	fivm_disable();
	if(file_sig_head )
		kfree(file_sig_head);
	if(file_sig_table)
		kfree(file_sig_table);

	return 0 ;
}


